<?php
/**
 * phpwell web client
 *
 * Based on PEAR's HTTP_Request2, this http client provides an interface I
 * find more intuitive.
 *
 *
 * @package     phpwell
 * @subpackage  web
 * @author      Tom Atwell <klenwell@gmail.com>
 * @copyright   Copyright 2010, Tom Atwell <klenwell@gmail.com>
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @link        http://klenwell.com/is
 * @link        http://pear.php.net/manual/en/package.http.http-request2.php
 *
 * NOTES
 * Set ssl_verify_peer to false - see http://bugs.php.net/47030
 *
 */

/**
 * Base web client class. Provides basic REST operations.
 *
 * @category   pear
 * @package    phpwell
 * @subpackage web
 * @author     Tom Atwell <klenwell@gmail.com>
 */
class HttpClient {

    /**
     *
     */
    var $version                    = '0.2';
    var $client_id                  = 'Phpwell-Client';

    /**
     *
     */
    var $PearClient                 = NULL;
    var $Response                   = NULL;
    var $request_url                = '';
    var $error_on_unexpected_code   = 1;

    /**
     *
     */
    var $LogList                    = array();
    var $ResponseList               = array();

    /**
     * redirect settings
     */
    var $num_redirects              = 0;
    var $max_redirects              = 10;

    /**
     *
     */
    function __construct() {
        $this->_load_pear_client();
        $this->client_id = sprintf('%s-v%s', $this->client_id, $this->version);
    }

    /**
     *
     */


    /**
     * Request Methods
     * @link    http://pear.php.net/manual/en/package.http.http-request2.request.php
     */
    function _http_request()
    {
        $Response = NULL;
        $Response = $this->PearClient->send();
        $Response->request_url = $this->PearClient->getUrl()->getUrl();
        $Response->status      = $Response->getStatus();
        $Response->status_code = floor($Response->status / 100);
        $Response->HeaderList = $Response->getHeader();
        $Response->CookieList = $Response->getCookies();
        $Response->CookieDict = $this->cookie_list_to_dict($Response->CookieList);
        return $Response;
    }

    function _request_url($url, $ParamDict=array(), $HeaderDict=array(), $is_redirect=0)
    {
        $method = $this->PearClient->getMethod();

        if ( $is_redirect ) {
            $this->num_redirects++;
        }

        $url = $this->add_parameters($url, $ParamDict, $method);
        $this->PearClient->setUrl($url);
        $this->request_url = $url;

        if ( ! empty($HeaderDict) ) {
            $this->PearClient->setHeader($HeaderDict);
        }

        try {
            $this->Response = $this->_http_request();
            $this->ResponseList[] = array(
                'request_url'   => $this->Response->request_url,
                'method'        => $method,
                'status'        => $this->Response->getStatus(),
                'is_redirect'   => (int) $this->Response->isRedirect(),
                'num_redirects' => $this->num_redirects,
                'HeaderList'    => $this->Response->getHeader(),
                'CookieList'    => $this->Response->getCookies(),
                'CookieDict'    => $this->Response->CookieDict,
                'body'          => $this->Response->getBody(),
            );
        }
        catch (HTTP_Request2_Exception $e) {
            $this->Response->status_code = 0;
            $this->Response->request_url = $this->PearClient->getUrl()->getUrl();
            $this->Response->error = sprintf('http exception: %s', $e->getMessage());
            $this->log($this->Response->error, 'error');
            return $this->trigger_request_error($this->Response->error);
        }

        /**
         * handle redirects
         */
        if ( $this->Response->status_code == 3 &&
             $this->num_redirects < $this->max_redirects )
        {
            if ( $redirect_url = $this->Response->getHeader('Location') )
            {
                $m = sprintf('redirect code %s: redirect to %s',
                             $this->Response->getStatus(),
                             $redirect_url);
                $this->log($m);
                return $this->_request_url($redirect_url, $ParamDict, $HeaderDict, 1);
            }
            else
            {
                $m = sprintf('redirect code %s: Location header not found',
                             $this->Response->getStatus());
                $this->log($m);
                return $this->trigger_request_error($m);
            }
        }
        elseif ( $this->Response->status_code == 3 )
        {
            $m = sprintf('redirect code %s: redirect limit %s exceeded',
                         $this->Response->getStatus(),
                         $this->max_redirects);
            return $this->trigger_request_error($m);
        }

        /**
         * handle 4xx, 5xx codes
         */
        if ( in_array($this->Response->status_code, array(4,5)) &&
             $this->error_on_unexpected_code )
        {
            $m = sprintf('unexpected response code [%s]: %s',
                         $this->Response->getStatus(),
                         $this->Response->getReasonPhrase());
            $this->log($m, 'error');
            return $this->trigger_request_error($m);
        }

        return $this->Response->getBody();
    }

    function get($url, $ParamDict=array(), $HeaderDict=array()) {
        $this->num_redirects = 0;
        $this->ResponseList = array();
        $this->PearClient->setMethod(HTTP_Request2::METHOD_GET);
        return $this->_request_url($url, $ParamDict, $HeaderDict);
    }

    /**
     *
     */
    function post($url, $ParamDict=array(), $HeaderDict=array()) {
        $this->num_redirects = 0;
        $this->ResponseList = array();
        $this->PearClient->setMethod(HTTP_Request2::METHOD_POST);
        return $this->_request_url($url, $ParamDict, $HeaderDict);
    }

    /**
     *
     */
    function set_client_config($key, $value) {
        return $this->PearClient->setConfig(array( $key => $value ));
    }

    /**
     *
     */
    function add_parameters($url, $ParamDict=array(), $method=HTTP_Request2::METHOD_GET) {
        if ( $method == HTTP_Request2::METHOD_POST ) {
            return $this->add_post_parameters($url, $ParamDict);
        }
        else {
            return $this->add_url_query_string($url, $ParamDict);
        }
    }

    function add_post_parameters($url, $ParamDict) {
        $this->PearClient->addPostParameter($ParamDict);
        return $url;
    }

    function add_url_query_string($url, $ParamDict) {
        if ( empty($ParamDict) ) {
            return $url;
        }

        $this->PearClient->setUrl($url);
        $UrlObject = $this->PearClient->getUrl();
        $UrlObject->setQueryVariables($ParamDict);
        return $UrlObject->getUrl();
    }

    /**
     * Cookie list to dict
     * Note: in event of cookies with same key, first will be overwritten by
     * second, hence the reason for both a list and a dict
     */
    function cookie_list_to_dict($CookieList) {
        $CookieDict = array();
        foreach ( $CookieList as $Cookie ) {
            $CookieDict[$Cookie['name']] = $Cookie['value'];
        }
        return $CookieDict;
    }

    /**
     *
     */
    function log($message, $type='debug') {
        $this->LogList[microtime(1)] = $message;
        if ( function_exists('log_kw') ) {
            log_well($message, $type);
        }
    }

    /**
     *
     */
    function _load_pear_client() {
        if ( is_null($this->PearClient) ) {
            require_once 'HTTP/Request2.php';
            $this->PearClient = new HTTP_Request2();

            # set redirect configs
            $this->set_client_config('follow_redirects', 0);
            $this->set_client_config('ssl_verify_peer', 0);
            #$this->set_client_config('max_redirects', $this->max_redirects);

            # set observer
            #$this->PearClient->attach(new HttpClientRedirectOberver($this));
        }
        return $this->PearClient;
    }

    /**
     *
     */
    function trigger_request_error($message='none')
    {
        list($file, $line, $fx) = $this->caller();
        $m = sprintf('[%s %s l.%s] %s', $fx, $file, $line, $message);
        $this->log($m, 'error');
        trigger_error($m, E_USER_WARNING);
        return 0;
    }

    function caller($back=2) {
        $Trace = debug_backtrace();
        #$this->pr($Trace);
        $f = basename($Trace[$back]['file']);
        $l = $Trace[$back]['line'];
        $fx = $Trace[$back]['function'];
        return array($f, $l, $fx);
    }

}


class HttpClientRedirectOberver implements SplObserver {

    var $Client = NULL;
    var $EventList = array();

    public function __construct($Client)
    {
        $this->Client = $Client;
    }

    public function update(SplSubject $subject)
    {
        $Event = $subject->getLastEvent();
        $this->Client->EventList[] = array($Event['name'], $Event['data']);
        if ( $Event['name'] == 'receivedHeaders' ) {
            $Response = $Event['data'];
            $this->Client->ResponseList[] = array(
                'status'        => $Response->getStatus(),
                'HeaderList'    => $Response->getHeader(),
                'CookieList'    => $Response->getCookies(),
                'is_redirect'   => (int) $Response->isRedirect(),
                'body'          => (string) $Response->getBody(),
            );
        }
    }
}
